/*
 $Id$

 Copyright (C) 2006-2007 by David Cotton

 This program is free software; you can redistribute it and/or modify it under
 the terms of the GNU General Public License as published by the Free Software
 Foundation; either version 2 of the License, or (at your option) any later
 version.

 This program is distributed in the hope that it will be useful, but WITHOUT
 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

 You should have received a copy of the GNU General Public License along with
 this program; if not, write to the Free Software Foundation, Inc., 51 Franklin
 Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */
package fr.free.jchecs.core;

import static fr.free.jchecs.core.SANUtils.toSAN;
import static fr.free.jchecs.core.board.BoardFactory.State.STARTING;
import static fr.free.jchecs.core.board.BoardFactory.Type.FASTEST;
import static fr.free.jchecs.core.board.FENUtils.toFEN;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;

import fr.free.jchecs.core.board.BoardFactory;
import fr.free.jchecs.core.move.Move;
import fr.free.jchecs.core.move.MoveGenerator;

/**
 * Description of a running game.
 * 
 * @author David Cotton
 */
public final class Game {
	/** Time (ms) allocated to a player for one game. */
	private static final long GAME_DURATION = 15 * 60 * 1000;

	/** Support related properties. */
	final PropertyChangeSupport _propertyChangeSupport = new PropertyChangeSupport(
			this);

	/** Description of the black player. */
	private final Player _blackPlayer = new Player(false);

	/** List of opinions from the board. */
	private final List<MoveGenerator> _positions = new ArrayList<MoveGenerator>();

	/** List of movements executed. */
	private final List<Move> _moves = new ArrayList<Move>();

	/** List of notations SAN movements. */
	private final List<String> _sanMoves = new ArrayList<String>();

	/** Description of the white player. */
	private final Player _whitePlayer = new Player(true);

	/** Current value of timer blacks. */
	long _blackTimer;

	/** Current movement (>= 0). */
	private int _currentMove;

	/** Current position (> 0). */
	private int _currentPosition;

	/** Value of the last statement of the timer. */
	long _lastTimerTick;

	/** Timer clock. */
	private Timer _timer;

	/** Current value of timer whites */
	long _whiteTimer;

	/**
	 * Instantiates a new game.
	 */
	public Game() {
		resetTo(BoardFactory.valueOf(FASTEST, STARTING));
	}

	/**
	 * Adds an object to listen to property changes.
	 * 
	 * @param property
	 *            Property to listen.
	 * @param listener
	 *            Object to add to listen.
	 */
	public void addPropertyChangeListener(final String property,
			final PropertyChangeListener listener) {
		assert property != null;
		assert listener != null;

		_propertyChangeSupport.addPropertyChangeListener(property, listener);
	}

	/**
	 * Reference the current description of the chessboard.
	 * 
	 * @return Current description of the chessboard.
	 */
	public MoveGenerator getBoard() {
		assert (_currentPosition > 0)
				&& (_currentPosition <= _positions.size());

		return _positions.get(_currentPosition - 1);
	}

	/**
	 * Reference to any movement in progress.
	 * 
	 * @return Current movement (or null if none).
	 */
	public Move getCurrentMove() {
		Move res = null;

		if ((_currentMove > 0) && (_currentMove <= _moves.size())) {
			res = _moves.get(getCurrentMoveIndex());
		}

		return res;
	}

	/**
	 * Reference index of current movement.
	 * 
	 * @return Index of the current movement.
	 */
	public int getCurrentMoveIndex() {
		assert (_currentMove >= 0) && (_currentMove <= _moves.size());
		return _currentMove - 1;
	}

	/**
	 * Returns the string corresponding to the FEN position.
	 * 
	 * @return FEN string from the current position.
	 */
	public String getFENPosition() {
		assert (_currentPosition > 0)
				&& (_currentPosition <= _positions.size());

		return toFEN(getBoard());
	}

	/**
	 * Returns the number of movements stored.
	 * 
	 * @return Number of movements.
	 */
	public int getMovesCount() {
		return _moves.size();
	}

	/**
	 * Reference all movements juqu current movement.
	 * 
	 * @return List of current movement to movement.
	 */
	public Move[] getMovesToCurrent() {
		final Move[] res = new Move[_currentMove];

		for (int i = 0; i < _currentMove; i++) {
			res[i] = _moves.get(i);
		}

		return res;
	}

	/**
	 * Reference the definition of player corresponding to a color.
	 * 
	 * @param isWhite
	 *            A "true " for the white player, "false " for black.
	 * @return Corresponding player.
	 */
	public Player getPlayer(final boolean isWhite) {
		if (isWhite) {
			return _whitePlayer;
		}

		return _blackPlayer;
	}

	/**
	 * Renvoi la liste des chaînes SAN correspondant aux mouvements.
	 * 
	 * @return Liste des chaînes SAN.
	 */
	public String[] getSANStrings() {
		return _sanMoves.toArray(new String[_sanMoves.size()]);
	}

	/**
	 * Renvoi la chaîne FEN de la position de départ.
	 * 
	 * @return Chaîne FEN de la position de départ.
	 */
	public String getStartingPosition() {
		return toFEN(_positions.get(0));
	}

	/**
	 * Renvoi l'état de la partie en cours.
	 * 
	 * @return Etat de la partie.
	 */
	public State getState() {
		final State res;

		final MoveGenerator etat = getBoard();
		final boolean trait = etat.isWhiteActive();
		if (etat.getValidMoves(trait).length == 0) {
			if (etat.isInCheck(trait)) {
				if (trait) {
					res = State.BLACK_MATES;
				} else {
					res = State.WHITE_MATES;
				}
			} else {
				res = State.STALEMATE;
			}
		} else if (etat.getHalfmoveCount() > 50) {
			res = State.DRAWN_BY_50_MOVE_RULE;
		} else {
			final MoveGenerator enCours = getBoard();
			int rep = 0;
			for (int i = 0; i < _currentPosition; i++) {
				if (enCours.equals(_positions.get(i))) {
					rep++;
					if (rep >= 3) {
						return State.DRAWN_BY_TRIPLE_REPETITION;
					}
				}
			}

			res = State.IN_PROGRESS;
		}

		return res;
	}

	/**
	 * Reference the current value of a timer for a player.
	 * 
	 * @param pCouleur
	 *            A "true" pour le timer des blancs, à "false" pour les noirs.
	 * @return Valeur courante du timer correspondant.
	 */
	public long getTimer(final boolean pCouleur) {
		if (pCouleur) {
			return _whiteTimer;
		}

		return _blackTimer;
	}

	/**
	 * Go to first movement.
	 */
	public void goFirst() {
		if (_currentMove > 0) {
			_currentMove = 0;
			_currentPosition = 1;
			_propertyChangeSupport.firePropertyChange("position", null, null);
		}
	}

	/**
	 * Go to the last movement.
	 */
	public void goLast() {
		final int s = _moves.size();
		if (_currentMove < s) {
			_currentMove = s;
			_currentPosition = _positions.size();
			_propertyChangeSupport.firePropertyChange("position", null, null);
		}
	}

	/**
	 * Go to the next movement.
	 */
	public void goNext() {
		if (_currentMove < _moves.size()) {
			_currentMove++;
			_currentPosition++;
			_propertyChangeSupport.firePropertyChange("position", null, null);
		}
	}

	/**
	 * Go to the previous movement.
	 */
	public void goPrevious() {
		if (_currentMove > 0) {
			_currentMove--;
			_currentPosition--;
			_propertyChangeSupport.firePropertyChange("position", null, null);
		}
	}

	/**
	 * Adds a movement from the current position.
	 * 
	 * @param pMouvement
	 *            Movement to add.
	 */
	public void moveFromCurrent(final Move pMouvement) {
		assert pMouvement != null;

		while (_moves.size() > _currentMove) {
			_moves.remove(_moves.size() - 1);
			_sanMoves.remove(_sanMoves.size() - 1);
		}
		while (_positions.size() > _currentPosition) {
			_positions.remove(_positions.size() - 1);
		}

		MoveGenerator state = getBoard();
		final boolean trait = state.isWhiteActive();
		final StringBuilder san = new StringBuilder();
		if (trait) {
			san.append(state.getFullmoveNumber()).append(". ");
		}
		san.append(toSAN(state, pMouvement));
		san.append(' ');
		state = state.derive(pMouvement, true);
		_positions.add(state);
		_currentPosition = _positions.size();
		_moves.add(pMouvement);
		_currentMove = _moves.size();

		switch (getState()) {
		case IN_PROGRESS:
			break;
		case WHITE_MATES:
			san.append("1-0");
			break;
		case BLACK_MATES:
			san.append("0-1");
			break;
		case STALEMATE:
			san.append("1/2-1/2");
			break;
		case DRAWN_BY_50_MOVE_RULE:
		case DRAWN_BY_TRIPLE_REPETITION:
			san.append("1/2-1/2 {Repetition}");
			break;
		default:
			assert false;
		}
		_sanMoves.add(san.toString());

		assert _moves.size() == _sanMoves.size();
		assert _positions.size() == (_moves.size() + 1);
		_propertyChangeSupport.firePropertyChange("position", null, null);
	}

	/**
	 * Removes an object to listen to property changes.
	 * 
	 * @param pPropriete
	 *            Property to listen.
	 * @param pEcouteur
	 *            Object to add to listen.
	 */
	public void removePropertyChangeListener(final String pPropriete,
			final PropertyChangeListener pEcouteur) {
		assert pPropriete != null;
		assert pEcouteur != null;

		_propertyChangeSupport.removePropertyChangeListener(pPropriete,
				pEcouteur);
	}

	/**
	 * Reset the description of the party to the position transmitted.
	 * 
	 * @param pEtat
	 *            New state of the chessboard.
	 */
	public void resetTo(final MoveGenerator pEtat) {
		assert pEtat != null;

		_moves.clear();
		_sanMoves.clear();
		_currentMove = _moves.size();
		_positions.clear();
		_positions.add(pEtat);
		_currentPosition = _positions.size();
		if (_timer != null) {
			_timer.cancel();
		}
		_blackTimer = GAME_DURATION;
		_whiteTimer = GAME_DURATION;
		_lastTimerTick = System.currentTimeMillis();
		_timer = new Timer();

		_propertyChangeSupport.firePropertyChange("position", null, null);
		_timer.scheduleAtFixedRate(new TimerTask() {
			/**
			 * Action triggered periodically by the timer clock manager.
			 */
			@Override
			public void run() {
				if (getState() == State.IN_PROGRESS) {
					final long time = System.currentTimeMillis();
					if (getBoard().isWhiteActive()) {
						_whiteTimer -= time - _lastTimerTick;
					} else {
						_blackTimer -= time - _lastTimerTick;
					}
					_lastTimerTick = time;
					_propertyChangeSupport.firePropertyChange("timer", null,
							null);
				}
			}
		}, 250, 1000);
	}

	/** Enumeration of possible states of a party. */
	public static enum State {
		/** Ongoing. */
		IN_PROGRESS,

		/** Victory blacks. */
		BLACK_MATES,

		/** Victory white. */
		WHITE_MATES,

		/** Stalemate. */
		STALEMATE,

		/** Completed after repeating the same position 3 times or more. */
		DRAWN_BY_TRIPLE_REPETITION,

		/** Completed by the 50 shots. */
		DRAWN_BY_50_MOVE_RULE;
	}
}
